package com.xyzwps.lib.openapi.element;

import com.xyzwps.lib.bedrock.UnimplementedException;
import com.xyzwps.lib.json.element.*;
import com.xyzwps.lib.json.JsonBuilder;

import java.util.Map;
import java.util.TreeSet;

import static com.xyzwps.lib.dollar.Dollar.*;

public class ToJsonVisitor implements OAEVisitor {

    private final JsonBuilder json = new JsonBuilder();

    public String toCompactString() {
        return json.toCompactString();
    }

    public String toPrettyString() {
        return json.toPrettyString();
    }

    @Override
    public void visit(Contact contact) {
        json.objectOpen();

        json.keyValue("name", contact.name());

        if (contact.url() != null) {
            json.comma();
            json.keyValue("url", contact.url());
        }

        if (contact.email() != null) {
            json.comma();
            json.keyValue("email", contact.email());
        }

        json.objectClose();
    }

    @Override
    public void visit(OpenApi doc) {
        json.objectOpen();

        json.keyValue("openapi", doc.openapi());

        json.comma();
        json.value("info").colon();
        doc.info().accept(this);

        var servers = doc.servers();
        if (servers != null && !servers.isEmpty()) {
            json.comma();
            json.value("servers").colon().arrayOpen();
            int i = 0;
            for (var server : servers) {
                if (i++ > 0) {
                    json.comma();
                }
                server.accept(this);
            }
            json.arrayClose();
        }

        var paths = doc.paths();
        if (paths != null) {
            json.comma();
            json.value("paths").colon();
            paths.accept(this);
        }

        var tags = doc.tags();
        if (tags != null && !tags.isEmpty()) {
            json.comma();
            json.value("tags").colon().arrayOpen();
            int i = 0;
            for (var tag : tags) {
                if (i++ > 0) {
                    json.comma();
                }
                tag.accept(this);
            }
            json.arrayClose();
        }

        var externalDocs = doc.externalDocs();
        if (externalDocs != null) {
            json.comma();
            json.value("externalDocs").colon();
            externalDocs.accept(this);
        }

        var components = doc.components();
        if (!components.isEmpty()) {
            json.comma();
            json.value("components").colon();
            components.accept(this);
        }

        json.objectClose();
    }

    @Override
    public void visit(Info info) {
        json.objectOpen();

        json.keyValue("title", info.title());

        if (info.summary() != null) {
            json.comma();
            json.keyValue("summary", info.summary());
        }

        if (info.description() != null) {
            json.comma();
            json.keyValue("description", info.description());
        }

        if (info.termsOfService() != null) {
            json.comma();
            json.keyValue("termsOfService", info.termsOfService());
        }

        if (info.contact() != null) {
            json.comma();
            json.value("contact").colon();
            info.contact().accept(this);
        }

        if (info.license() != null) {
            json.comma();
            json.value("license").colon();
            info.license().accept(this);
        }

        json.comma();
        json.keyValue("version", info.version());

        json.objectClose();
    }

    @Override
    public void visit(License license) {
        json.objectOpen();

        json.keyValue("name", license.name());

        switch (license) {
            case License.IdLicense idLicense -> {
                if (idLicense.identifier() != null) {
                    json.comma();
                    json.keyValue("identifier", idLicense.identifier());
                }
            }
            case License.UrlLicense urlLicense -> {
                if (urlLicense.url() != null) {
                    json.comma();
                    json.keyValue("url", urlLicense.url());
                }
            }
        }

        json.objectClose();
    }

    @Override
    public void visit(ExternalDocumentation d) {
        json.objectOpen();

        if (d.description() != null) {
            json.keyValue("description", d.description()).comma();
        }

        json.keyValue("url", d.url());

        json.objectClose();
    }

    @Override
    public void visit(Paths paths) {
        var pathSet = paths.pathSet();
        json.objectOpen();

        var firstOrComma = new FirstOrComma();

        for (var path : pathSet) {
            firstOrComma.tryComma();
            json.value(path).colon();
            var item = paths.item(path);
            if (item != null) {
                item.accept(this);
            }
        }

        json.objectClose();
    }

    @Override
    public void visit(Server server) {
        json.objectOpen();

        json.keyValue("url", server.url());

        if (server.description() != null) {
            json.comma();
            json.keyValue("description", server.description());
        }

        var variables = server.variables();
        if (variables != null && !variables.isEmpty()) {
            json.comma();
            json.value("variables").colon().objectOpen();

            var keySet = new TreeSet<>(variables.keySet());
            int i = 0;
            for (var key : keySet) {
                if (i++ > 0) {
                    json.comma();
                }
                json.value(key).colon();
                server.variables().get(key).accept(this);
            }

            json.objectClose();
        }

        json.objectClose();
    }

    @Override
    public void visit(ServerVariable v) {
        json.objectOpen();

        json.keyValue("default", v.defaultValue());

        var enums = v.enums();
        if (enums != null && !enums.isEmpty()) {
            json.comma().value("enum").colon().arrayOpen();

            int i = 0;
            for (var e : enums) {
                if (i++ > 0) {
                    json.comma();
                }
                json.value(e);
            }

            json.arrayClose();
        }

        if (v.description() != null) {
            json.comma();
            json.keyValue("description", v.description());
        }

        json.objectClose();

    }

    @Override
    public void visit(Tag tag) {
        json.objectOpen();

        json.keyValue("name", tag.name());

        if (tag.description() != null) {
            json.comma();
            json.keyValue("description", tag.description());
        }

        if (tag.externalDocs() != null) {
            json.comma();
            json.value("externalDocs").colon();
            tag.externalDocs().accept(this);
        }

        json.objectClose();
    }

    @Override
    public void visit(Operation op) {
        json.objectOpen();

        var firstOrComma = new FirstOrComma();

        var tags = op.tags();
        if (tags != null && !tags.isEmpty()) {
            firstOrComma.tryComma();
            json.value("tags").colon().arrayOpen();
            int i = 0;
            for (var tag : tags) {
                if (i++ > 0) json.comma();
                json.value(tag);
            }
            json.arrayClose();
        }

        if (op.summary() != null) {
            firstOrComma.tryComma();
            json.keyValue("summary", op.summary());
        }

        if (op.description() != null) {
            firstOrComma.tryComma();
            json.keyValue("description", op.description());
        }

        var externalDocs = op.externalDocs();
        if (externalDocs != null) {
            firstOrComma.tryComma();
            json.value("externalDocs").colon();
            externalDocs.accept(this);
        }

        var operationId = op.operationId();
        if (operationId != null) {
            firstOrComma.tryComma();
            json.keyValue("operationId", operationId);
        }

        var deprecated = op.deprecated();
        if (deprecated) {
            firstOrComma.tryComma();
            json.keyValue("deprecated", deprecated);
        }

        var parameters = op.parameters();
        if (parameters != null && !parameters.isEmpty()) {
            firstOrComma.tryComma();
            json.value("parameters").colon().arrayOpen();
            int i = 0;
            for (var parameter : parameters) {
                if (i++ > 0) json.comma();
                parameter.accept(this);
            }
            json.arrayClose();
        }

        var requestBody = op.requestBody();
        if (requestBody != null) {
            firstOrComma.tryComma();
            json.value("requestBody").colon();
            requestBody.accept(this);
        }

        var responses = op.responses();
        if (responses != null) {
            firstOrComma.tryComma();
            json.value("responses").colon();
            responses.accept(this);
        }

        var security = op.security();
        if (security != null && !security.isEmpty()) {
            firstOrComma.tryComma();
            json.value("security").colon().arrayOpen();
            int i = 0;
            for (var requirement : security) {
                if (i++ > 0) json.comma();
                requirement.accept(this);
            }
            json.arrayClose();
        }

        var servers = op.servers();
        if (servers != null && !servers.isEmpty()) {
            firstOrComma.tryComma();
            json.value("servers").colon().arrayOpen();
            int i = 0;
            for (var server : servers) {
                if (i++ > 0) json.comma();
                server.accept(this);
            }
            json.arrayClose();
        }

        json.objectClose();
    }

    @Override
    public void visit(PathItem item) {
        json.objectOpen();

        var firstOrComma = new FirstOrComma();

        var $ref = item.$ref();
        if ($ref != null) {
            firstOrComma.tryComma();
            json.keyValue("$ref", $ref);
        }

        var summary = item.summary();
        if (summary != null) {
            firstOrComma.tryComma();
            json.keyValue("summary", summary);
        }

        var description = item.description();
        if (description != null) {
            firstOrComma.tryComma();
            json.keyValue("description", description);
        }

        var get = item.get();
        if (get != null) {
            firstOrComma.tryComma();
            json.value("get").colon();
            get.accept(this);
        }

        var post = item.post();
        if (post != null) {
            firstOrComma.tryComma();
            json.value("post").colon();
            post.accept(this);
        }

        var put = item.put();
        if (put != null) {
            firstOrComma.tryComma();
            json.value("put").colon();
            put.accept(this);
        }

        var delete = item.delete();
        if (delete != null) {
            firstOrComma.tryComma();
            json.value("delete").colon();
            delete.accept(this);
        }

        var head = item.head();
        if (head != null) {
            firstOrComma.tryComma();
            json.value("head").colon();
            head.accept(this);
        }

        var patch = item.patch();
        if (patch != null) {
            firstOrComma.tryComma();
            json.value("patch").colon();
            patch.accept(this);
        }

        var trace = item.trace();
        if (trace != null) {
            firstOrComma.tryComma();
            json.value("trace").colon();
            trace.accept(this);
        }

        var servers = item.servers();
        if (servers != null && !servers.isEmpty()) {
            firstOrComma.tryComma();
            json.value("servers").colon().arrayOpen();
            int i = 0;
            for (var server : servers) {
                if (i++ > 0) json.comma();
                server.accept(this);
            }
            json.arrayClose();
        }

        var parameters = item.parameters();
        if (parameters != null && !parameters.isEmpty()) {
            firstOrComma.tryComma();
            json.value("parameters").colon().arrayOpen();
            int i = 0;
            for (var p : parameters) {
                if (i++ > 0) json.comma();
                p.accept(this);
            }
            json.arrayClose();
        }

        json.objectClose();
    }

    @Override
    public void visit(Parameter p) {
        json.objectOpen();

        json.keyValue("name", p.name());
        json.comma();
        json.keyValue("in", p.in().name().toLowerCase());

        if (p.description() != null) {
            json.comma();
            json.keyValue("description", p.description());
        }

        if (p.required()) {
            json.comma();
            json.keyValue("required", p.required());
        }

        if (p.deprecated()) {
            json.comma();
            json.keyValue("deprecated", p.deprecated());
        }

        if (p.schema() != null) {
            json.comma();
            json.value("schema").colon();
            p.schema().accept(this);
        }

        if (p.style() != null) {
            json.comma();
            json.keyValue("style", p.style().value);
        }

        json.objectClose();
    }

    @Override
    public void visit(Reference r) {
        json.objectOpen();
        json.keyValue("$ref", r.$ref());
        json.objectClose();
    }

    @Override
    public void visit(Responses response) {
        json.objectOpen();

        var firstOrComma = new FirstOrComma();

        var statusResponses = response.statusResponses();
        if (statusResponses != null && !statusResponses.isEmpty()) {
            var keySet = new TreeSet<>(statusResponses.keySet());
            for (var key : keySet) {
                firstOrComma.tryComma();
                json.value(key + "").colon();
                statusResponses.get(key).accept(this);
            }
        }

        var defaultResponse = response.responseDefault();
        if (defaultResponse != null) {
            firstOrComma.tryComma();
            json.value("default").colon();
            defaultResponse.accept(this);
        }

        json.objectClose();
    }

    @Override
    public void visit(RequestBody body) {
        json.objectOpen();

        var firstOrComma = new FirstOrComma();

        var description = body.description();
        if (description != null) {
            firstOrComma.tryComma();
            json.keyValue("description", description);
        }

        var content = body.content();
        if (content != null && !content.isEmpty()) {
            firstOrComma.tryComma();
            json.value("content").colon().objectOpen();
            int i = 0;
            var keySet = new TreeSet<>(content.keySet());
            for (var key : keySet) {
                if (i++ > 0) json.comma();
                json.value(key).colon();
                content.get(key).accept(this);
            }
            json.objectClose();
        }

        var required = body.required();
        if (required) {
            firstOrComma.tryComma();
            json.keyValue("required", required);
        }

        json.objectClose();
    }

    private final class FirstOrComma {
        boolean isFirst = true;

        void tryComma() {
            if (!isFirst) json.comma();
            isFirst = false;
        }
    }

    @Override
    public void visit(Response it) {

        json.objectOpen();

        json.keyValue("description", it.description());

        var headers = it.headers();
        if (headers != null && !headers.isEmpty()) {
            json.comma();
            json.value("headers").colon().objectOpen();
            var keySet = new TreeSet<>(headers.keySet());
            int i = 0;
            for (var key : keySet) {
                if (i++ > 0) json.comma();
                json.value(key).colon();
                headers.get(key).accept(this);
            }
            json.objectClose();
        }

        var content = it.content();
        if (content != null && !content.isEmpty()) {
            json.comma();
            json.value("content").colon().objectOpen();
            var keySet = new TreeSet<>(content.keySet());
            int i = 0;
            for (var key : keySet) {
                if (i++ > 0) json.comma();
                json.value(key).colon();
                var mediaType = content.get(key);
                mediaType.accept(this);
            }
            json.objectClose();
        }

        var links = it.links();
        if (links != null && !links.isEmpty()) {
            json.comma();
            json.value("links").colon().objectOpen();

            var keySet = new TreeSet<>(links.keySet());
            int i = 0;
            for (var key : keySet) {
                if (i++ > 0) json.comma();
                json.value(key).colon();
                links.get(key).accept(this);
            }
            json.objectClose();
        }


        json.objectClose();
    }

    @Override
    public void visit(Link link) {
        throw UnimplementedException.todo();
    }

    @Override
    public void visit(Style style) {
        throw UnimplementedException.todo();
    }

    @Override
    public void visit(Header header) {
        throw UnimplementedException.todo();
    }

    @Override
    public void visit(Schema schema) {
        schema.accept(new ToJsonSchemaVisitor(this));
    }

    private class ToJsonSchemaVisitor implements SchemaVisitor {

        private final ToJsonVisitor toJsonVisitor;

        ToJsonSchemaVisitor(ToJsonVisitor visitor) {
            this.toJsonVisitor = visitor;
        }

        @Override
        public void visit(Schema.RefSchema it) {
            json.objectOpen();
            json.keyValue("$ref", it.$ref());
            json.objectClose();
        }

        @Override
        public void visit(Schema.AllOf it) {
            json.objectOpen();

            var firstOrComma = new FirstOrComma();

            var title = it.title();
            if ($.isNotBlank(title)) {
                firstOrComma.tryComma();
                json.keyValue("title", title);
            }

            var description = it.description();
            if ($.isNotBlank(description)) {
                firstOrComma.tryComma();
                json.keyValue("description", description);
            }

            var deprecated = it.deprecated();
            if (deprecated != null && deprecated) {
                firstOrComma.tryComma();
                json.keyValue("deprecated", true);
            }

            var schemas = it.schemas();
            firstOrComma.tryComma();
            json.value("allOf").colon();
            json.arrayOpen();
            int i = 0;
            for (var s : schemas) {
                if (i++ > 0) json.comma();
                s.accept(this);
            }
            json.arrayClose();

            json.objectClose();
        }

        @Override
        public void visit(Schema.OneOf it) {
            json.objectOpen();

            var firstOrComma = new FirstOrComma();

            var title = it.title();
            if ($.isNotBlank(title)) {
                firstOrComma.tryComma();
                json.keyValue("title", title);
            }

            var description = it.description();
            if ($.isNotBlank(description)) {
                firstOrComma.tryComma();
                json.keyValue("description", description);
            }

            var deprecated = it.deprecated();
            if (deprecated != null && deprecated) {
                firstOrComma.tryComma();
                json.keyValue("deprecated", true);
            }
            var schemas = it.schemas();
            firstOrComma.tryComma();
            json.value("oneOf").colon();
            json.arrayOpen();
            int i = 0;
            for (var s : schemas) {
                if (i++ > 0) json.comma();
                s.accept(this);
            }
            json.arrayClose();

            json.objectClose();
        }

        @Override
        public void visit(Schema.AnyOf it) {
            json.objectOpen();

            var firstOrComma = new FirstOrComma();

            var title = it.title();
            if ($.isNotBlank(title)) {
                firstOrComma.tryComma();
                json.keyValue("title", title);
            }

            var description = it.description();
            if ($.isNotBlank(description)) {
                firstOrComma.tryComma();
                json.keyValue("description", description);
            }

            var deprecated = it.deprecated();
            if (deprecated != null && deprecated) {
                firstOrComma.tryComma();
                json.keyValue("deprecated", true);
            }

            var schemas = it.schemas();
            firstOrComma.tryComma();
            json.value("anyOf").colon();
            json.arrayOpen();
            int i = 0;
            for (var s : schemas) {
                if (i++ > 0) json.comma();
                s.accept(this);
            }
            json.arrayClose();

            json.objectClose();
        }

        @Override
        public void visit(Schema.ObjectSchema it) {
            json.objectOpen();

            json.keyValue("type", "object");

            var title = it.title();
            if ($.isNotBlank(title)) {
                json.comma();
                json.keyValue("title", title);
            }

            var description = it.description();
            if ($.isNotBlank(description)) {
                json.comma();
                json.keyValue("description", description);
            }

            var deprecated = it.deprecated();
            if (deprecated != null && deprecated) {
                json.comma();
                json.keyValue("deprecated", true);
            }

            var discriminator = it.discriminator();
            if (discriminator != null) {
                json.comma();
                json.value("discriminator").colon();
                discriminator.accept(toJsonVisitor);
            }

            var properties = it.properties();
            if (properties != null && !properties.isEmpty()) {
                json.comma();
                json.value("properties").colon().objectOpen();
                var keySet = new TreeSet<>(properties.keySet());
                int i = 0;
                for (var key : keySet) {
                    if (i++ > 0) json.comma();
                    json.value(key).colon();
                    properties.get(key).accept(this);
                }
                json.objectClose();
            }

            var required = it.required();
            if (required != null && !required.isEmpty()) {
                json.comma();
                json.value("required").colon().arrayOpen();
                var keySet = new TreeSet<>(required);
                int i = 0;
                for (var key : keySet) {
                    if (i++ > 0) json.comma();
                    json.value(key);
                }
                json.arrayClose();
            }

            var maxProperties = it.maxProperties();
            if (maxProperties != null) {
                json.comma();
                json.keyValue("maxProperties", maxProperties);
            }

            var minProperties = it.minProperties();
            if (minProperties != null) {
                json.comma();
                json.keyValue("minProperties", minProperties);
            }

            var additionalProperties = it.additionalProperties();
            if (additionalProperties != null) {
                json.comma();
                json.value("additionalProperties").colon();
                additionalProperties.accept(this);
            }

            var example = it.example();
            if (example != null) {
                json.comma();
                json.value("example").colon();
                toJsonVisitor.visit(example);
            }

            json.objectClose();
        }

        @Override
        public void visit(Schema.ArraySchema it) {
            json.objectOpen();

            json.keyValue("type", "array");

            var title = it.title();
            if ($.isNotBlank(title)) {
                json.comma();
                json.keyValue("title", title);
            }

            var description = it.description();
            if ($.isNotBlank(description)) {
                json.comma();
                json.keyValue("description", description);
            }

            var deprecated = it.deprecated();
            if (deprecated != null && deprecated) {
                json.comma();
                json.keyValue("deprecated", true);
            }

            var items = it.items();
            if (items != null) {
                json.comma();
                json.value("items").colon();
                items.accept(this);
            }

            var example = it.example();
            if (example != null && !example.isEmpty()) {
                json.comma();
                json.value("example").colon();
                toJsonVisitor.visit(example);
            }

            var maxItems = it.maxItems();
            if (maxItems != null) {
                json.comma();
                json.keyValue("maxItems", maxItems);
            }

            var minItems = it.minItems();
            if (minItems != null) {
                json.comma();
                json.keyValue("minItems", minItems);
            }

            var uniqueItems = it.uniqueItems();
            if (uniqueItems != null && uniqueItems) {
                json.comma();
                json.keyValue("uniqueItems", true);
            }

            var maxContains = it.maxContains();
            if (maxContains != null) {
                json.comma();
                json.keyValue("maxContains", maxContains);
            }

            var minContains = it.minContains();
            if (minContains != null) {
                json.comma();
                json.keyValue("minContains", minContains);
            }

            json.objectClose();
        }

        @Override
        public void visit(Schema.EnumSchema it) {
            json.objectOpen();

            json.keyValue("type", "string");

            var title = it.title();
            if ($.isNotBlank(title)) {
                json.comma();
                json.keyValue("title", title);
            }

            var description = it.description();
            if ($.isNotBlank(description)) {
                json.comma();
                json.keyValue("description", description);
            }

            var deprecated = it.deprecated();
            if (deprecated != null && deprecated) {
                json.comma();
                json.keyValue("deprecated", true);
            }

            var example = it.example();
            if ($.isNotBlank(example)) {
                json.comma();
                json.keyValue("example", example);
            }

            var enums = it.enums();
            json.comma();
            json.value("enum").colon().arrayOpen();
            int i = 0;
            for (var e : enums) {
                if (i++ > 0) json.comma();
                json.value(e);
            }
            json.arrayClose();

            json.objectClose();
        }

        @Override
        public void visit(Schema.StringSchema it) {
            json.objectOpen();

            json.keyValue("type", "string");

            var title = it.title();
            if ($.isNotBlank(title)) {
                json.comma();
                json.keyValue("title", title);
            }

            var description = it.description();
            if ($.isNotBlank(description)) {
                json.comma();
                json.keyValue("description", description);
            }

            var deprecated = it.deprecated();
            if (deprecated != null && deprecated) {
                json.comma();
                json.keyValue("deprecated", true);
            }

            var format = it.format();
            if (format != null && format.value() != null) {
                json.comma();
                json.keyValue("format", format.value());
            }

            var defaultValue = it.defaultValue();
            if ($.isNotBlank(defaultValue)) {
                json.comma();
                json.keyValue("default", defaultValue);
            }

            var example = it.example();
            if ($.isNotBlank(example)) {
                json.comma();
                json.keyValue("example", example);
            }

            var maxLength = it.maxLength();
            if (maxLength != null) {
                json.comma();
                json.keyValue("maxLength", maxLength);
            }

            var minLength = it.minLength();
            if (minLength != null) {
                json.comma();
                json.keyValue("minLength", minLength);
            }

            var pattern = it.pattern();
            if ($.isNotBlank(pattern)) {
                json.comma();
                json.keyValue("pattern", pattern);
            }

            json.objectClose();
        }

        @Override
        public void visit(Schema.BooleanSchema it) {
            json.objectOpen();

            json.keyValue("type", "boolean");

            var title = it.title();
            if ($.isNotBlank(title)) {
                json.comma();
                json.keyValue("title", title);
            }

            var description = it.description();
            if ($.isNotBlank(description)) {
                json.comma();
                json.keyValue("description", description);
            }

            var deprecated = it.deprecated();
            if (deprecated != null && deprecated) {
                json.comma();
                json.keyValue("deprecated", true);
            }

            json.objectClose();
        }

        @Override
        public void visit(Schema.NumberSchema it) {
            json.objectOpen();
            json.keyValue("type", "number");

            var title = it.title();
            if ($.isNotBlank(title)) {
                json.comma();
                json.keyValue("title", title);
            }

            var description = it.description();
            if ($.isNotBlank(description)) {
                json.comma();
                json.keyValue("description", description);
            }

            var deprecated = it.deprecated();
            if (deprecated != null && deprecated) {
                json.comma();
                json.keyValue("deprecated", true);
            }

            var defaultValue = it.defaultValue();
            if (defaultValue != null) {
                json.comma();
                json.keyValue("default", defaultValue);
            }

            var format = it.format();
            if (format != null && format.value() != null) {
                json.comma();
                json.keyValue("format", format.value());
            }

            var minimum = it.minimum();
            if (minimum != null) {
                json.comma();
                json.keyValue("minimum", minimum);
            }

            var maximum = it.maximum();
            if (maximum != null) {
                json.comma();
                json.keyValue("maximum", maximum);
            }

            var exclusiveMinimum = it.exclusiveMinimum();
            if (exclusiveMinimum != null) {
                json.comma();
                json.keyValue("exclusiveMinimum", exclusiveMinimum);
            }

            var exclusiveMaximum = it.exclusiveMaximum();
            if (exclusiveMaximum != null) {
                json.comma();
                json.keyValue("exclusiveMaximum", exclusiveMaximum);
            }

            var example = it.example();
            if (example != null) {
                json.comma();
                json.keyValue("example", example);
            }

            json.objectClose();
        }

        @Override
        public void visit(Schema.IntegerSchema it) {
            json.objectOpen();
            json.keyValue("type", "integer");

            var title = it.title();
            if ($.isNotBlank(title)) {
                json.comma();
                json.keyValue("title", title);
            }

            var description = it.description();
            if ($.isNotBlank(description)) {
                json.comma();
                json.keyValue("description", description);
            }

            var deprecated = it.deprecated();
            if (deprecated != null && deprecated) {
                json.comma();
                json.keyValue("deprecated", true);
            }

            var defaultValue = it.defaultValue();
            if (defaultValue != null) {
                json.comma();
                json.keyValue("default", defaultValue);
            }

            var format = it.format();
            if (format != null && format.value() != null) {
                json.comma();
                json.keyValue("format", format.value());
            }

            var minimum = it.minimum();
            if (minimum != null) {
                json.comma();
                json.keyValue("minimum", minimum);
            }

            var maximum = it.maximum();
            if (maximum != null) {
                json.comma();
                json.keyValue("maximum", maximum);
            }

            var exclusiveMinimum = it.exclusiveMinimum();
            if (exclusiveMinimum != null) {
                json.comma();
                json.keyValue("exclusiveMinimum", exclusiveMinimum);
            }

            var exclusiveMaximum = it.exclusiveMaximum();
            if (exclusiveMaximum != null) {
                json.comma();
                json.keyValue("exclusiveMaximum", exclusiveMaximum);
            }

            var example = it.example();
            if (example != null) {
                json.comma();
                json.keyValue("example", example);
            }

            json.objectClose();
        }
    }

    // TODO: 改用 JsonElement 的 visitor
    private void visit(JsonObject jo) {
        json.objectOpen();

        var keySet = new TreeSet<>(jo.keySet());
        int i = 0;
        for (var key : keySet) {
            if (i++ > 0) json.comma();
            json.value(key).colon();
            switch (jo.get(key)) {
                case JsonObject x -> visit(x);
                case JsonArray x -> visit(x);
                case JsonString s -> json.value(s.value());
                case JsonBoolean b -> json.value(b.value);
                case JsonInteger j -> json.value(j.value());
                case JsonDecimal d -> json.value(d.value());
                case JsonNull ignored -> json.nullValue();
                case null -> json.nullValue();
            }
        }

        json.objectClose();
    }

    private void visit(JsonArray ja) {
        json.arrayOpen();

        for (var i = 0; i < ja.size(); i++) {
            if (i > 0) json.comma();
            switch (ja.get(i)) {
                case JsonObject x -> visit(x);
                case JsonArray x -> visit(x);
                case JsonString s -> json.value(s.value());
                case JsonBoolean b -> json.value(b.value);
                case JsonInteger j -> json.value(j.value());
                case JsonDecimal d -> json.value(d.value());
                case JsonNull ignored -> json.nullValue();
                case null -> json.nullValue();
            }
        }

        json.arrayClose();
    }

    @Override
    public void visit(Example example) {
        json.objectOpen();

        var firstOrComma = new FirstOrComma();

        var summary = example.summary();
        if (summary != null) {
            firstOrComma.tryComma();
            json.keyValue("summary", summary);
        }

        var description = example.description();
        if (description != null) {
            firstOrComma.tryComma();
            json.keyValue("description", description);
        }

        var value = example.value();
        if (value != null) {
            firstOrComma.tryComma();
            switch (value) {
                case String s -> json.keyValue("value", s);
                case Map x -> {
                    // TODO: 太丑了
                    Map<String, String> m = (Map<String, String>) x;
                    json.value("value").colon().objectOpen();
                    var keySet = new TreeSet<>(m.keySet());
                    int i = 0;
                    for (var key : keySet) {
                        if (i++ > 0) json.comma();
                        var v = m.get(key);
                        json.keyValue(key, v);
                    }
                    json.objectClose();
                }
                default -> throw new IllegalStateException("Unsupported value type: " +
                        value.getClass().getCanonicalName());
            }
        }

        var externalValue = example.externalValue();
        if (externalValue != null) {
            firstOrComma.tryComma();
            json.keyValue("externalValue", externalValue);
        }

        json.objectClose();
    }

    @Override
    public void visit(Encoding encoding) {
        throw UnimplementedException.todo();
    }

    @Override
    public void visit(MediaType it) {
        json.objectOpen();

        var firstOrComma = new FirstOrComma();

        var schema = it.schema();
        if (schema != null) {
            firstOrComma.tryComma();
            json.value("schema").colon();
            schema.accept(this);
        }

        var example = it.example();
        if (example != null) {
            firstOrComma.tryComma();
            json.keyValue("example", example);
        }

        var examples = it.examples();
        if (examples != null && !examples.isEmpty()) {
            firstOrComma.tryComma();
            json.value("examples").colon().objectOpen();
            var keySet = new TreeSet<>(examples.keySet());
            int i = 0;
            for (var key : keySet) {
                if (i++ > 0) json.comma();
                json.value(key).colon();
                examples.get(key).accept(this);
            }
            json.objectClose();
        }

        var encoding = it.encoding();
        if (encoding != null && !encoding.isEmpty()) {
            firstOrComma.tryComma();
            json.value("encoding").colon().objectOpen();
            var keySet = new TreeSet<>(encoding.keySet());
            int i = 0;
            for (var key : keySet) {
                if (i++ > 0) json.comma();

                json.value(key).colon();
                var enc = encoding.get(key);
                enc.accept(this);
            }
            json.objectClose();
        }

        json.objectClose();
    }

    @Override
    public void visit(SecurityRequirement requirement) {
        json.objectOpen();

        var keySet = new TreeSet<>(requirement.requirements().keySet());
        int i = 0;
        for (var key : keySet) {
            if (i++ > 0) json.comma();
            json.value(key).colon().arrayOpen();
            var scopes = requirement.requirements().get(key);
            int j = 0;
            for (var scope : scopes) {
                if (j++ > 0) json.comma();
                json.value(scope);
            }
            json.arrayClose();
        }

        json.objectClose();
    }

    @Override
    public void visit(Components components) {
        json.objectOpen();

        var schemas = components.schemas();
        if (schemas != null && !schemas.isEmpty()) {
            json.value("schemas").colon().objectOpen();
            var keySet = new TreeSet<>(schemas.keySet());
            int i = 0;
            for (var key : keySet) {
                if (i++ > 0) json.comma();
                json.value(key).colon();
                schemas.get(key).accept(this);
            }
            json.objectClose();
        }

        json.objectClose();
    }

    @Override
    public void visit(Discriminator discriminator) {
        json.objectOpen();

        json.keyValue("propertyName", discriminator.propertyName());

        var mapping = discriminator.mapping();
        if (mapping != null && !mapping.isEmpty()) {
            json.value("mapping").colon().objectOpen();
            var keySet = new TreeSet<>(mapping.keySet());
            int i = 0;
            for (var key : keySet) {
                if (i++ > 0) json.comma();
                json.keyValue(key, mapping.get(key));
            }
            json.objectClose();
        }

        json.objectClose();
    }
}
